54. 绘制曲线图

⭐ 曲线图:数据可视化的基石

曲线图(Line Plot)是最基础也是最重要的数据可视化形式之一。

在金融与商业领域,曲线图帮助我们:

  • 观察趋势:识别价格的上升、下降、震荡趋势
  • 发现模式:识别季节性、周期性模式
  • 比较表现:对比不同资产的表现
  • 监控变化:实时跟踪关键指标的变化

⭐ 曲线图的数学基础

曲线图本质上是函数的可视化

\[ y = f(x) \]

在金融时间序列中:

  • 自变量 \(x\):时间 \(t\)
  • 因变量 \(y\):价格、收益率、成交量等

⭐ 连续与离散的处理

  • 理论上,价格是离散的(仅在交易时刻有定义)
  • 为了可视化,我们用线段连接相邻点,形成连续曲线
  • 这种线性插值假设价格在交易间隔内平滑变化

⭐ 平台任务1:等额本息还款曲线

Listing 1
# 注:numpy_financial包本地未安装,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import pandas as pd  # 导入Pandas数据分析库
import matplotlib.pyplot as plt    #导入matplotlab的子模块pyplot
import  numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30   #贷款的期限(年)
principle = 6e6 #贷款的本金
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end")  # 计算等额本息月供金额
print("在等额本息规则下丁先生每月偿还的金额",-round(pay_month,2))  # 输出在等额本息规则下丁先生每月偿还的金额
t = np.arange(n*12)+1  #生成一个包含每次还款期限长度的数组
principle_pay_month = nmf.ppmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end") #计算每月偿还的本金额
interest_pay_month = nmf.ipmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end")  #计算每月偿还的利息额
pay_month_array = pay_month*np.ones_like(principle_pay_month) #创建一个每月偿还的数组
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(t,-pay_month_array,"r-",label=u"每月偿还金额",lw=2.5)  # 绑制折线图
plt.plot(t,-principle_pay_month,"m--",label=u"每月偿还本金金额",lw=2.5)  # 绑制折线图
plt.plot(t,-interest_pay_month,"b--",label =u"每月产股韩利息金额",lw=2.5)  # 绑制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14)  # 设置Y轴标签
plt.title(u"等额本息还款规则下每月偿还的金额以及本金额与利息额")  # 设置图表标题
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("1.png")  # 保存图形至文件

⭐ 平台任务2:不同利率下的月供变化

Listing 2
# 注:numpy_financial包本地未安装,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  #导入matplotlab的子模块pyplot
import numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30   #贷款的期限(年)
principle = 6e6 #贷款的本金

r_list = np.linspace(0.02,0.08,100) #生成贷款利率的一个数组
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end")  # 计算等额本息月供金额
pay_month_list = nmf.pmt(rate=r_list/12,nper=n*12,pv=principle,fv=0,when="end") #计算不同贷款利率条件下的每月偿还本息之和
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(r_list,-pay_month_list,"r-",label=u"每月偿还金额",lw=2.5)  # 绑制折线图
plt.plot(r,-pay_month,"bo",label=u"贷款利率4.9%的每月偿还金额")  # 绑制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"贷款利率",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=14)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90)  # 设置Y轴标签
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("2.png")  # 保存图形至文件

⭐ 平台任务3:等额本金还款曲线

Listing 3
# 注:numpy_financial包本地未安装,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt #导入matplotlab的子模块pyplot
import numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30  #贷款的期限(年)
principle = 6e6 #贷款的本金
t = np.arange(n*12)+1 #生成一个包含每次还款期限长度的数组
prin_month = principle/(n*12) #计算等额本金还款规则下的每月本金还款额
prin_month_array = np.ones(n*12)*prin_month #生成一个每月本金还款额的数组
int_month_array = np.zeros_like(prin_month_array) #生成存放每月利息还款额的初始数组
for i in np.arange(n*12):  # 遍历np.arange(n*12)中的每个i
 int_month_array[i] = (principle-i*prin_month)*r/12 #计算逐月支付的利息额
pay_total_month = prin_month_array+int_month_array #j计算等额本金还款规则下的每月还款总额
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(t,pay_total_month,"m-",label=u"每月偿还金额",lw=2.5)  # 绑制折线图
plt.plot(t,prin_month_array,"y--",label=u"每月偿还本金额",lw=2.5)  # 绑制折线图
plt.plot(t,int_month_array,"c--",label =u"每月偿还利息额",lw=2.5)  # 绑制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=14)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90)  # 设置Y轴标签
plt.title(u"等额本金还款规则下每月偿还的金额以及本金额与利息额",fontsize=14)  # 设置图表标题
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("3.png")  # 保存图形至文件

⭐ 基础曲线图:核心函数 plt.plot()

绘制基础曲线图的核心步骤:

  1. 导入库:matplotlib.pyplot
  2. 准备数据:X轴(时间)和Y轴(数值)
  3. 调用 plt.plot(x, y) 绑制曲线
  4. 添加装饰:标题、轴标签、网格线等
  5. 显示图形:plt.show()

⭐ 中文字体设置(必备)

在绑制包含中文的图表前,必须设置中文字体:

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
  • SimHei:黑体,确保中文正常显示
  • unicode_minus:解决负号 - 显示为方块的问题

⭐ 基础曲线图示例:股价走势

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 模拟贵州茅台30个交易日的股价数据
dates = pd.date_range('2024-01-01', periods=30)
prices = [1850 + i * 2 + np.random.randn() * 10 for i in range(30)]
df = pd.DataFrame({'日期': dates, '收盘价': prices})

plt.figure(figsize=(12, 6))
plt.plot(df['日期'], df['收盘价'], linewidth=2, color='#2E86AB')
plt.title('贵州茅台股价走势', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('收盘价(元)', fontsize=12)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Figure 1: 基础曲线图——贵州茅台股价走势

plt.plot() 常用参数

参数 说明 示例
linewidth 线宽,数值越大线越粗 linewidth=2
color 颜色(十六进制/RGB/英文名) color='#2E86AB'
linestyle 线型 '-' 实线、'--' 虚线、':' 点线
label 图例标签 label='收盘价'
alpha 透明度(0~1) alpha=0.7

⭐ 图表装饰函数一览

函数 作用
plt.title() 设置图表标题
plt.xlabel() / plt.ylabel() 设置轴标签
plt.grid() 显示网格线
plt.legend() 显示图例
plt.xticks(rotation=45) 旋转X轴刻度标签
plt.tight_layout() 自动调整布局

⭐ 多条曲线对比:在同一图中绑制多条线

核心思路:多次调用 plt.plot(),每次绑制一条曲线。

关键设计要点:

  • 颜色选择:使用对比度高、易于区分的颜色
  • 线宽区分:主要数据用较粗的线,次要数据用较细的线
  • 图例plt.legend(loc='best') 自动选择最佳位置

⭐ 多条曲线示例:三只股票对比

stocks_data = pd.DataFrame({
    '日期': dates,
    '贵州茅台': [1850 + i * 1.5 + np.random.randn() * 8 for i in range(30)],
    '五粮液': [220 + i * 0.8 + np.random.randn() * 5 for i in range(30)],
    '招商银行': [45 + i * 0.3 + np.random.randn() * 2 for i in range(30)]
})

plt.figure(figsize=(12, 6))
plt.plot(stocks_data['日期'], stocks_data['贵州茅台'],
         label='贵州茅台', linewidth=2, color='#E3120B')
plt.plot(stocks_data['日期'], stocks_data['五粮液'],
         label='五粮液', linewidth=2, color='#008080')
plt.plot(stocks_data['日期'], stocks_data['招商银行'],
         label='招商银行', linewidth=2, color='#2C3E50')

plt.title('不同股票价格走势对比', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('收盘价(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Figure 2: 多条曲线对比——不同股票价格走势

⭐ 多曲线对比注意事项

当数据量级差异大时(如茅台1850元 vs 招商银行45元),考虑:

  • 双Y轴:左右各一个Y轴,对应不同量级
  • 归一化:将所有数据转换为相对百分比
  • 子图(subplots):分别绘制在不同图中

⭐ 双Y轴图:展示量价关系

应用场景:同时展示不同量级的数据

  • 价格(几十元) vs 成交量(几千手)
  • 收益率 vs 波动率
  • 股价 vs 技术指标

核心函数:ax1.twinx() 创建共享X轴的右Y轴

⭐ 双Y轴示例:价格与成交量

df_dual = pd.DataFrame({
    '日期': dates,
    '收盘价': [45 + i * 0.5 + np.random.randn() * 3 for i in range(30)],
    '成交量': [5000 + i * 100 + np.random.randn() * 500 for i in range(30)]
})

fig, ax1 = plt.subplots(figsize=(12, 6))

color1 = '#E3120B'
ax1.set_xlabel('日期', fontsize=12)
ax1.set_ylabel('收盘价(元)', color=color1, fontsize=12)
line1 = ax1.plot(df_dual['日期'], df_dual['收盘价'],
                 color=color1, linewidth=2, label='收盘价')
ax1.tick_params(axis='y', labelcolor=color1)
ax1.grid(True, alpha=0.3)

ax2 = ax1.twinx()
color2 = '#008080'
ax2.set_ylabel('成交量(手)', color=color2, fontsize=12)
line2 = ax2.plot(df_dual['日期'], df_dual['成交量'],
                 color=color2, linewidth=2, linestyle='--', label='成交量')
ax2.tick_params(axis='y', labelcolor=color2)

lines = line1 + line2
labels = [l.get_label() for l in lines]
ax1.legend(lines, labels, loc='best', fontsize=11)
plt.title('价格与成交量走势', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()
Figure 3: 双Y轴图——价格与成交量

⭐ 双Y轴使用注意事项

  • 双Y轴可能误导读者,需谨慎使用
  • 明确标注哪个轴对应哪条线(颜色一致)
  • 考虑用子图(subplots)替代,信息更清晰
  • 避免两个Y轴刻度范围差异过大

⭐ 填充区域图:展示数据区间

plt.fill_between() 用于填充两条曲线之间的区域。

金融应用

  • 布林带:移动平均线 ± 标准差
  • 置信区间:统计模型预测的上下限
  • 期权策略:到期时的盈亏区间
  • VaR区间:在险价值的区间表示

⭐ 填充区域图示例:价格预测区间

df_area = pd.DataFrame({
    '日期': dates,
    '价格': [100 + i + np.random.randn() * 3 for i in range(30)],
    '上限': [105 + i + np.random.randn() * 2 for i in range(30)],
    '下限': [95 + i + np.random.randn() * 2 for i in range(30)]
})

plt.figure(figsize=(12, 6))
plt.plot(df_area['日期'], df_area['价格'],
         label='实际价格', linewidth=2, color='#2C3E50')
plt.fill_between(df_area['日期'], df_area['下限'], df_area['上限'],
                 alpha=0.3, color='#008080', label='预测区间')
plt.title('价格走势与预测区间', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Figure 4: 填充区域图——价格区间

fill_between 常用参数

参数 说明 示例
alpha 透明度(0~1),越小越透明 alpha=0.3
color 填充颜色 color='#008080'
where 条件填充,只填充满足条件的区域 where=(y1 > y2)
interpolate 在边界处插值,使填充更平滑 interpolate=True

⭐ 堆叠面积图:展示构成变化

plt.stackplot() 绘制堆叠面积图,展示各部分随时间的变化。

金融应用

  • 资产配置:股票、债券、现金的配置变化
  • 收入构成:主营业务收入、其他业务收入
  • 市场份额:不同公司在市场中的份额变化

⭐ 堆叠面积图的数学含义

每层的累积值:

\[ y_{\text{total}} = y_1 + y_2 + \cdots + y_n \]

\(i\) 层的显示范围:

\[ y_{\text{layer } i} = \sum_{j=1}^{i} y_j \]

⭐ 堆叠面积图示例:投资组合

df_portfolio = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=12),
    '股票': [100 + i*5 + np.random.randn()*3 for i in range(12)],
    '债券': [80 + i*2 + np.random.randn()*2 for i in range(12)],
    '现金': [20 + i*0.5 + np.random.randn()*1 for i in range(12)]
})

plt.figure(figsize=(12, 6))
plt.stackplot(df_portfolio['日期'],
              df_portfolio['股票'],
              df_portfolio['债券'],
              df_portfolio['现金'],
              alpha=0.8,
              colors=['#E3120B', '#008080', '#F0A700'],
              labels=['股票', '债券', '现金'])
plt.title('投资组合资产配置变化', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('资产价值(万元)', fontsize=12)
plt.legend(loc='upper left', fontsize=11)
plt.grid(True, alpha=0.3, axis='y')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Figure 5: 堆叠面积图——投资组合构成

⭐ 对数坐标轴:处理指数增长数据

线性坐标:展示绝对值变化

对数坐标:展示相对变化(百分比)

在金融中的意义:

  • 复利效应:对数坐标下,恒定增长率呈直线
  • 收益率比较:不同起始点的资产可直接比较斜率
  • 长期趋势:更适合展示跨越数量级的数据

⭐ 对数坐标的数学原理

\[ \ln(y) = \ln(A \cdot e^{rt}) = \ln(A) + rt \]

在半对数图上,指数增长呈直线,斜率即为增长率 \(r\)

核心函数:ax.semilogy()plt.yscale('log')

⭐ 对数坐标示例:复利增长

years = np.arange(2000, 2024)
initial_value = 100
growth_rate = 0.15
values = initial_value * (1 + growth_rate) ** (years - 2000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# 线性坐标(左图)
ax1.plot(years, values, linewidth=2, color='#E3120B')
ax1.set_title('线性坐标', fontsize=14, fontweight='bold')
ax1.set_xlabel('年份', fontsize=12)
ax1.set_ylabel('价值(元)', fontsize=12)
ax1.grid(True, alpha=0.3)

# 对数坐标(右图)
ax2.semilogy(years, values, linewidth=2, color='#008080')
ax2.set_title('对数坐标', fontsize=14, fontweight='bold')
ax2.set_xlabel('年份', fontsize=12)
ax2.set_ylabel('价值(元,对数)', fontsize=12)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f'初始值: {initial_value}元')
print(f'年化增长率: {growth_rate:.1%}')
print(f'2023年价值: {values[-1]:.2f}元')
print(f'总增长倍数: {values[-1]/initial_value:.1f}倍')
Figure 6: 对数Y轴——复利增长可视化
初始值: 100元
年化增长率: 15.0%
2023年价值: 2489.15元
总增长倍数: 24.9倍

⭐ 金融应用:移动平均线

简单移动平均(SMA)是技术分析的基础:

\[ \text{SMA}_t = \frac{1}{n}\sum_{i=0}^{n-1} P_{t-i} \]

核心函数:.rolling(window=n).mean()

常用周期:MA5(短期)、MA20(中期)、MA60(长期)

⭐ 交易信号:金叉与死叉

  • 金叉(Golden Cross):短期均线上穿长期均线 → 买入信号
  • 死叉(Death Cross):短期均线下穿长期均线 → 卖出信号

局限性

  • 滞后性:基于历史价格,信号滞后
  • 震荡市场中频繁产生错误信号
  • 参数敏感性:不同周期效果差异大

⭐ 移动平均线示例:含交叉信号

np.random.seed(42)
df_price = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=60),
    '收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 60))
})

# 计算移动平均线
df_price['MA5'] = df_price['收盘价'].rolling(window=5).mean()
df_price['MA20'] = df_price['收盘价'].rolling(window=20).mean()

plt.figure(figsize=(14, 7))
plt.plot(df_price['日期'], df_price['收盘价'],
         label='收盘价', linewidth=1.5, color='#2C3E50', alpha=0.7)
plt.plot(df_price['日期'], df_price['MA5'],
         label='MA5', linewidth=1.5, color='#E3120B')
plt.plot(df_price['日期'], df_price['MA20'],
         label='MA20', linewidth=1.5, color='#008080')

# 标注金叉和死叉
golden_cross = (df_price['MA5'] > df_price['MA20']) & \
               (df_price['MA5'].shift(1) <= df_price['MA20'].shift(1))
death_cross = (df_price['MA5'] < df_price['MA20']) & \
              (df_price['MA5'].shift(1) >= df_price['MA20'].shift(1))

plt.scatter(df_price['日期'][golden_cross], df_price['MA5'][golden_cross],
            color='red', s=100, marker='^', label='金叉', zorder=5)
plt.scatter(df_price['日期'][death_cross], df_price['MA5'][death_cross],
            color='green', s=100, marker='v', label='死叉', zorder=5)

plt.title('移动平均线与交叉信号', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Figure 7: 移动平均线与交叉信号

⭐ 本章小结

图表类型 核心函数 适用场景
基础曲线图 plt.plot() 单条时间序列
多条曲线 多次 plt.plot() 对比不同资产
双Y轴图 ax1.twinx() 不同量级数据
填充区域图 plt.fill_between() 预测区间/布林带
堆叠面积图 plt.stackplot() 构成变化
对数坐标 ax.semilogy() 指数增长数据